AI编程之开发Qwen-Image UI小工具

开发Qwen-Image 文生图 UI 工具

在魔搭社区获得了API key,可以调用推理模型API,单个模型500次,一天2000次调用还算可以。cherry studio没研究明白为啥接入不了图像生成模型?智谱的倒是可以。随后看着魔搭文档说明在python里试用了一下Qwen-Image模型,生成图片还挺好用,大概20秒生成一张,质量也不错,但又不习惯在代码里反复修改来生成图片,所以就使用AI编写了一个小工具。

目前小工具实现的功能

  • 输入图片描述
  • 选择生成的图片风格(模型内置)
  • 选择分辨率(模型内置)
  • 选择输出输出路径
  • 生成图片后立即预览

使用Gemini-2.5-Pro/Flash开发

pVWaeKK.png

哈gemi哈气了

pVWabqO.png

最终python打包后的UI界面(简洁但实用)

pVWaVv6.png

随意生成了一些图片,还是挺满意的

gril.jpg
测试.jpg
cat.jpg
边牧.jpg
wind.jpg

代码备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import requests
import time
import json
from PIL import Image, ImageTk
from io import BytesIO
import threading
import os

class ImageGeneratorApp:
def __init__(self, root):
self.root = root
self.root.title("AI图片生成工具")
self.root.geometry("600x550") # 稍微增加高度以容纳所有组件
self.root.resizable(False, False)

# API配置 (请确保您的API Key是有效的)
self.base_url = 'https://api-inference.modelscope.cn/'
self.api_key = "ms-7d5daba1-fcff-4a52-8f4f-0a6176cfadd4" # 示例Key,请替换为您自己的

# 创建界面
self.create_widgets()

def create_widgets(self):
main_frame = tk.Frame(self.root)
main_frame.pack(padx=20, pady=10, fill=tk.BOTH, expand=True)

# 标题
title_label = tk.Label(main_frame, text="AI图片生成工具", font=("Arial", 16, "bold"))
title_label.pack(pady=10)

# 输入区域
input_frame = ttk.LabelFrame(main_frame, text="设置")
input_frame.pack(pady=5, fill=tk.X)
input_frame.columnconfigure(0, weight=1) # 使内部组件可以水平扩展

tk.Label(input_frame, text="图片描述:", font=("Arial", 10)).grid(row=0, column=0, sticky=tk.W, pady=(10, 2), padx=10)
prompt_frame = tk.Frame(input_frame)
prompt_frame.grid(row=1, column=0, pady=5, padx=10, sticky="ew")
self.prompt_entry = tk.Text(prompt_frame, height=3, wrap=tk.WORD)
self.prompt_entry.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

prompt_scrollbar = ttk.Scrollbar(prompt_frame, orient=tk.VERTICAL, command=self.prompt_entry.yview)
prompt_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.prompt_entry.config(yscrollcommand=prompt_scrollbar.set)

# 风格选择
tk.Label(input_frame, text="图片风格:", font=("Arial", 10)).grid(row=2, column=0, sticky=tk.W, pady=5, padx=10)
self.style_var = tk.StringVar(value="真实风格")
style_combo = ttk.Combobox(input_frame, textvariable=self.style_var, state="readonly")
style_combo['values'] = ("真实风格", "像素风格", "油画风格", "水彩风格", "动漫风格", "科幻风格", "中国山水画风格")
style_combo.grid(row=3, column=0, pady=5, padx=10, sticky="ew")

# 分辨率选择
tk.Label(input_frame, text="分辨率 (宽x高):", font=("Arial", 10)).grid(row=4, column=0, sticky=tk.W, pady=5, padx=10)
self.resolution_var = tk.StringVar(value="1:1 (1328x1328)")
resolution_combo = ttk.Combobox(input_frame, textvariable=self.resolution_var, state="readonly")
resolution_combo['values'] = [
"1:1 (1328x1328)", "16:9 (1664x928)", "9:16 (928x1664)",
"4:3 (1472x1140)", "3:4 (1140x1472)", "3:2 (1584x1056)",
"2:3 (1056x1584)"
]
resolution_combo.grid(row=5, column=0, pady=5, padx=10, sticky="ew")

# 输出目录
tk.Label(input_frame, text="输出目录:", font=("Arial", 10)).grid(row=6, column=0, sticky=tk.W, pady=5, padx=10)
dir_frame = tk.Frame(input_frame)
dir_frame.grid(row=7, column=0, pady=2, padx=10, sticky="ew")

self.output_dir = tk.StringVar(value=os.path.join(os.getcwd(), "qwen-img")) # 默认输出目录
self.default_output_dir = os.path.join(os.getcwd(), "qwen-img") # 存储默认目录

self.use_default_dir_var = tk.BooleanVar(value=True) # 默认使用默认目录
self.use_default_dir_check = ttk.Checkbutton(dir_frame, text="使用默认输出目录",
variable=self.use_default_dir_var,
command=self.toggle_output_dir_widgets)
self.use_default_dir_check.pack(side=tk.LEFT, padx=(0, 10))

self.dir_label = tk.Label(dir_frame, textvariable=self.output_dir, relief=tk.SUNKEN, bd=1, anchor=tk.W)
self.dir_label.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=2)
self.browse_button = tk.Button(dir_frame, text="浏览...", command=self.browse_directory)
self.browse_button.pack(side=tk.RIGHT, padx=(5,0))

self.toggle_output_dir_widgets() # 初始化时设置组件状态

# 文件名
tk.Label(input_frame, text="保存文件名:", font=("Arial", 10)).grid(row=8, column=0, sticky=tk.W, pady=5, padx=10)
self.filename_entry = tk.Entry(input_frame)
self.filename_entry.insert(0, "generated_image.jpg")
self.filename_entry.grid(row=9, column=0, pady=(0, 10), padx=10, sticky="ew")

# 按钮
button_frame = tk.Frame(main_frame)
button_frame.pack(pady=10)

self.generate_button = tk.Button(button_frame, text="生成图片", command=self.generate_image,
bg="#4CAF50", fg="white", font=("Arial", 12, "bold"), padx=20, pady=5)
self.generate_button.pack(side=tk.LEFT, padx=10)

self.preview_button = tk.Button(button_frame, text="预览图片", command=self.preview_image,
bg="#2196F3", fg="white", font=("Arial", 12, "bold"), padx=20, pady=5)
self.preview_button.pack(side=tk.LEFT, padx=10)

# 状态和进度
status_frame = tk.Frame(main_frame)
status_frame.pack(pady=5, fill=tk.X)
self.status_label = tk.Label(status_frame, text="准备就绪", font=("Arial", 10), anchor='w')
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)

self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
self.progress_bar.pack(pady=5, fill=tk.X)

self.generated_image = None
self.image_path = None

def browse_directory(self):
directory = filedialog.askdirectory()
if directory:
self.output_dir.set(directory)
self.use_default_dir_var.set(False) # 如果手动选择了目录,则取消勾选“使用默认输出目录”
self.toggle_output_dir_widgets()

def toggle_output_dir_widgets(self):
if self.use_default_dir_var.get():
self.output_dir.set(self.default_output_dir)
self.dir_label.config(state=tk.DISABLED)
self.browse_button.config(state=tk.DISABLED)
else:
self.dir_label.config(state=tk.NORMAL)
self.browse_button.config(state=tk.NORMAL)

def generate_image(self):
prompt = self.prompt_entry.get("1.0", tk.END).strip()
if not prompt:
messagebox.showwarning("警告", "请输入图片描述!")
return

style = self.style_var.get()
resolution_str = self.resolution_var.get()

# --- 这是修正后的代码块 ---
try:
# 从 "1:1 (1328x1328)" 格式中提取 "1328*1328"
# 1. 分割字符串,例如 "1:1 (1328x1328)" -> ['1:1 ', '1328x1328)']
parts = resolution_str.split('(')
# 2. parts 是一个列表,我们需要第二个元素 parts[1],它是一个字符串 '1328x1328)'
# 然后对这个字符串进行处理,去掉右括号
size_str = parts[1].replace(')', '')

# 3. 将 "x" 替换为 "*",得到 API 需要的 "1328*1328" 格式
width, height = size_str.split('x')
resolution = f"{width}x{height}"

except (IndexError, ValueError):
messagebox.showerror("错误", "无效的分辨率格式!")
return
# --- 修正结束 ---

filename = self.filename_entry.get().strip()
if not filename:
filename = "generated_image.jpg"

output_path = os.path.join(self.output_dir.get(), filename)

# 禁用按钮并重置状态
self.generate_button.config(state=tk.DISABLED)
self.status_label.config(text="正在提交任务...")
self.progress_var.set(0) # 每次生成前重置进度条

# 在新线程中生成图片以避免UI卡死
thread = threading.Thread(target=self._generate_image_thread, args=(prompt, style, resolution, output_path))
thread.daemon = True
thread.start()

def _generate_image_thread(self, prompt, style, resolution, output_path):
try:
common_headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}

full_prompt = f"{prompt},风格:{style}"

self.root.after(0, lambda: self.status_label.config(text="正在生成图片,请稍候..."))

response = requests.post(
f"{self.base_url}v1/images/generations",
headers={**common_headers, "X-ModelScope-Async-Mode": "true"},
data=json.dumps({
"model": "Qwen/Qwen-Image", # 推荐使用更新的模型以获得更好效果
"prompt": full_prompt,
"size": resolution
}, ensure_ascii=False).encode('utf-8')
)

response.raise_for_status()
task_id = response.json()["task_id"]

self.root.after(0, lambda: self.progress_var.set(10))

while True:
result = requests.get(
f"{self.base_url}v1/tasks/{task_id}",
headers={**common_headers, "X-ModelScope-Task-Type": "image_generation"},
)
result.raise_for_status()
data = result.json()

if data["task_status"] == "SUCCEED":
self.root.after(0, lambda: self.status_label.config(text="生成成功,正在下载图片..."))
# 下载图片
image_url = data["output_images"][0]
image_response = requests.get(image_url)
image_response.raise_for_status()

image = Image.open(BytesIO(image_response.content))
image.save(output_path)

self.generated_image = image
self.image_path = output_path

# 更新UI
self.root.after(0, lambda: self.progress_var.set(100))
self.root.after(0, lambda: self.status_label.config(text=f"图片已保存至: {os.path.basename(output_path)}"))
self.root.after(0, lambda: self.generate_button.config(state=tk.NORMAL))
self.root.after(0, self.preview_image) # 自动预览
break
elif data["task_status"] == "FAILED":
error_message = data.get("output", {}).get("message", "未知错误")
self.root.after(0, lambda: messagebox.showerror("生成失败", f"错误: {error_message}"))
self.root.after(0, lambda: self.status_label.config(text="图片生成失败"))
self.root.after(0, lambda: self.generate_button.config(state=tk.NORMAL))
self.root.after(0, lambda: self.progress_bar.set(0))
break

# 根据任务状态更新进度条
status_to_progress = {
"QUEUED": 20,
"RUNNING": 50,
"SUCCEED": 100,
"FAILED": 0
}

current_status = data["task_status"]
if current_status in status_to_progress:
target_progress = status_to_progress[current_status]
current_progress = self.progress_var.get()
if current_progress < target_progress:
self.root.after(0, lambda: self.progress_var.set(target_progress))
elif current_status == "RUNNING" and self.progress_var.get() < 90:
# 如果是RUNNING状态,并且进度条还没到90%,则缓慢增加
self.root.after(0, lambda: self.progress_var.set(self.progress_var.get() + 5))

time.sleep(3) # 轮询间隔

except requests.exceptions.HTTPError as e:
error_info = f"HTTP错误: {e.response.status_code}\n{e.response.text}"
self.root.after(0, lambda: messagebox.showerror("API错误", error_info))
self.root.after(0, lambda: self.status_label.config(text="API请求失败"))
self.root.after(0, lambda: self.progress_var.set(0)) # 错误时重置进度条
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("未知错误", f"发生错误: {str(e)}"))
self.root.after(0, lambda: self.status_label.config(text=f"错误:{str(e)}"))
self.root.after(0, lambda: self.progress_var.set(0)) # 错误时重置进度条
finally:
# 确保按钮在任何情况下都会被重新启用
self.root.after(0, lambda: self.generate_button.config(state=tk.NORMAL))
self.root.after(0, lambda: self.progress_var.set(0)) # 确保在结束时重置进度条

def preview_image(self):
if self.generated_image:
preview_window = tk.Toplevel(self.root)
preview_window.title("图片预览")

screen_width = preview_window.winfo_screenwidth()
screen_height = preview_window.winfo_screenheight()

img = self.generated_image.copy()

# 缩放图片以适应屏幕,同时保持宽高比
max_width = int(screen_width * 0.8)
max_height = int(screen_height * 0.8)
img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)

photo = ImageTk.PhotoImage(img)

label = tk.Label(preview_window, image=photo)
label.image = photo # 必须保持对photo的引用,否则图片不显示
label.pack(padx=10, pady=10)

# 居中显示预览窗口
preview_window.update_idletasks() # 更新窗口信息
window_width = preview_window.winfo_width()
window_height = preview_window.winfo_height()
position_x = (screen_width - window_width) // 2
position_y = (screen_height - window_height) // 2
preview_window.geometry(f"+{position_x}+{position_y}")

else:
messagebox.showinfo("提示", "请先生成一张图片!")

if __name__ == "__main__":
# 确保您已安装必要的库: pip install requests Pillow
root = tk.Tk()
app = ImageGeneratorApp(root)
root.mainloop()

感慨

AI这东西就是好用啊,我一个非程序员也能有自己的小软件了。

接下来还有图生图,文本转语音,文本转视频,慢慢整~!AI真是太好玩了。